<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js raycaster - batch - lod - bvh</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
		<style>
			a {
				text-decoration: underline;
			}
		</style>
	</head>

	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> batch lod bvh - <a href="https://github.com/agargaro/batched-mesh-extensions" target="_blank" rel="noopener">@three.ez/batched-mesh-extensions</a><br/>
			BatchedMesh with 10 geometries and 500k instances.<br>
			Each geometry has 5 LODs (4 generated with meshoptimizer). <br>
			Frustum culling and raycasting are accelerated by using BVHs (TLAS & BLAS). <br>
		</div>

		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/",

					"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.0/build/index.module.js",

					"@three.ez/batched-mesh-extensions": "https://cdn.jsdelivr.net/npm/@three.ez/batched-mesh-extensions@0.0.8/build/webgl.js",
					"bvh.js": "https://cdn.jsdelivr.net/npm/bvh.js@0.0.13/build/index.js",

					"@three.ez/simplify-geometry": "https://cdn.jsdelivr.net/npm/@three.ez/simplify-geometry@0.0.1/build/index.js",
					"meshoptimizer": "https://cdn.jsdelivr.net/npm/meshoptimizer@0.23.0/+esm"
				}
			}
		</script>

		<script type="module">

			import * as THREE from 'three';
			import Stats from 'three/addons/libs/stats.module.js';
			import { MapControls } from 'three/addons/controls/MapControls.js';
			import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

			import { acceleratedRaycast, computeBatchedBoundsTree } from 'three-mesh-bvh';

			import { createRadixSort, extendBatchedMeshPrototype, getBatchedMeshLODCount } from '@three.ez/batched-mesh-extensions';
			import { performanceRangeLOD, simplifyGeometriesByErrorLOD } from '@three.ez/simplify-geometry';

			// add and override BatchedMesh methods ( @three.ez/batched-mesh-extensions )
			extendBatchedMeshPrototype();

			// add the extension functions ( three-mesh-bvh )
			THREE.Mesh.prototype.raycast = acceleratedRaycast;
			THREE.BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;

			let stats;
			let camera, scene, renderer;

			const instancesCount = 500000;
			let batchedMesh;
			let lastHoveredInstance = null;
			const lastHoveredColor = new THREE.Color();
			const highlight = new THREE.Color( 'red' );

			const raycaster = new THREE.Raycaster();
			const mouse = new THREE.Vector2( 1, 1 );
			const position = new THREE.Vector3();
			const quaternion = new THREE.Quaternion();
			const scale = new THREE.Vector3( 1, 1, 1 );
			const matrix = new THREE.Matrix4();
			const color = new THREE.Color();

			init();

			async function init() {

				// renderer
				renderer = new THREE.WebGLRenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.toneMapping = THREE.ACESFilmicToneMapping;
				renderer.toneMappingExposure = 0.8;
				document.body.appendChild( renderer.domElement );

				//

				scene = new THREE.Scene();

				const pmremGenerator = new THREE.PMREMGenerator( renderer );
				scene.environment = pmremGenerator.fromScene( new RoomEnvironment(), 0.04 ).texture;

				//

				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 1000 );
				camera.position.set( 0, 20, 55 );

				//

				raycaster.firstHitOnly = true;

				stats = new Stats();
				document.body.appendChild( stats.dom );

				const controls = new MapControls( camera, renderer.domElement );
				controls.maxPolarAngle = Math.PI / 2;

				const geometries = [
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 1 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 2 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 3 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 4 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 5 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 2, 1 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 2, 3 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 3, 1 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 4, 1 ),
					new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 5, 3 )
				];

				// generate 4 LODs (levels of detail) for each geometry
				const geometriesLODArray = await simplifyGeometriesByErrorLOD( geometries, 4, performanceRangeLOD );

				// create BatchedMesh
				const { vertexCount, indexCount, LODIndexCount } = getBatchedMeshLODCount( geometriesLODArray );
				batchedMesh = new THREE.BatchedMesh( instancesCount, vertexCount, indexCount, new THREE.MeshStandardMaterial( { metalness: 1, roughness: 0.8 } ) );
			
				// enable radix sort for better performance
				batchedMesh.customSort = createRadixSort( batchedMesh );

				// add geometries and their LODs to the batched mesh ( all LODs share the same position array )
				for ( let i = 0; i < geometriesLODArray.length; i ++ ) {

					const geometryLOD = geometriesLODArray[ i ];
					const geometryId = batchedMesh.addGeometry( geometryLOD[ 0 ], - 1, LODIndexCount[ i ] );
					batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 1 ], 50 );
					batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 2 ], 100 );
					batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 3 ], 125 );
					batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 4 ], 200 );
			
				}

				// place instances in a 2D grid with randomized rotation and color
				const sqrtCount = Math.ceil( Math.sqrt( instancesCount ) );
				const size = 5.5;
				const start = ( sqrtCount / - 2 * size ) + ( size / 2 );

				for ( let i = 0; i < instancesCount; i ++ ) {

					const r = Math.floor( i / sqrtCount );
					const c = i % sqrtCount;
					const id = batchedMesh.addInstance( Math.floor( Math.random() * geometriesLODArray.length ) );
					position.set( c * size + start, 0, r * size + start );
					quaternion.random();
					batchedMesh.setMatrixAt( id, matrix.compose( position, quaternion, scale ) );
					batchedMesh.setColorAt( id, color.setHSL( Math.random(), 0.6, 0.5 ) );
			
				}

				// compute blas (bottom-level acceleration structure) bvh ( three-mesh-bvh )
				batchedMesh.computeBoundsTree();

				// compute tlas (top-level acceleration structure) bvh ( @three.ez/batched-mesh-extensions )
				batchedMesh.computeBVH( THREE.WebGLCoordinateSystem );
			
				scene.add( batchedMesh );
			
				// set up gui
				const config = {
					freeze: false,
					useBVH: true,
					useLOD: true
				};
			
				const bvh = batchedMesh.bvh;
				const lods = batchedMesh._geometryInfo.map( x => x.LOD );
				const onBeforeRender = batchedMesh.onBeforeRender;
			
				const gui = new GUI();

				gui.add( batchedMesh, 'instanceCount' ).disable();
			
				gui.add( config, 'freeze' ).onChange( v => {

					batchedMesh.onBeforeRender = v ? () => {} : onBeforeRender;

				} );

				const frustumCullingFolder = gui.addFolder( 'Frustum culling & raycasting' );
				frustumCullingFolder.add( config, 'useBVH' ).onChange( v => {

					batchedMesh.bvh = v ? bvh : null;

				} );

				const geometriesFolder = gui.addFolder( 'Geometries' );
				geometriesFolder.add( config, 'useLOD' ).onChange( v => {

					const geometryInfo = batchedMesh._geometryInfo;
					for ( let i = 0; i < geometryInfo.length; i ++ ) {

						geometryInfo[ i ].LOD = v ? lods[ i ] : null;

					}

				} );

				document.addEventListener( 'pointermove', onPointerMove );
				window.addEventListener( 'resize', onWindowResize );
				onWindowResize();
			
				renderer.setAnimationLoop( animate );

			}


			function onPointerMove( event ) {

				event.preventDefault();

				mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
				mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

				raycast();

			}
			

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function raycast() {

				raycaster.setFromCamera( mouse, camera );
				const intersection = raycaster.intersectObject( batchedMesh );

				const batchId = intersection.length > 0 ? intersection[ 0 ].batchId : null;
			
				if ( lastHoveredInstance === batchId ) return;

				if ( lastHoveredInstance ) {

					batchedMesh.setColorAt( lastHoveredInstance, lastHoveredColor );
			
				}

				if ( batchId ) {

					batchedMesh.getColorAt( batchId, lastHoveredColor );
					batchedMesh.setColorAt( batchId, highlight );
			
				}

				lastHoveredInstance = batchId;

			}

			function animate() {

				stats.begin();

				renderer.render( scene, camera );

				stats.end();

			}

		</script>

	</body>
</html>
